﻿using OPCAutomation;
using System.Net.NetworkInformation;

namespace Zhaoxi.OpcLesson
{
    internal class Program
    {
        static OPCServer server;

        static void Main(string[] args)
        {
            // 客户端通信对象
            // OPCServer: Connect、GetOPCServers
            // OPCBrowser
            // OPCGroups、OPCGroup
            // OPCItems、OPCItem

            // 读取操作
            // 并不是创建一个服务器,而是需要通过这个对象连接到某个服务器
            server = new OPCServer();

            // 获取指定终端的OPC服务(方便连接到哪一个OPC服务)
            //dynamic servers = server.GetOPCServers();
            //foreach (string s in servers)
            //{
            //    Console.WriteLine(s);
            //}

            // 建立起一个OPC的连接,基于COM组件
            // 两个参数:ProgID 服务器的唯一编号
            //          Node   服务器地址(没有表示服务器地址,以本机去找服务；也可以提供IP/计算机名称)
            server.Connect("Kepware.KEPServerEX.V6"); // 默认本机的OPC服务器
            // Kepware可以安装在其他电脑上,但OPC DA通信,必须配置DCOM通信环境,两个电脑需要在一个windows域中
            //server.Connect("Kepware.KEPServerEX.V6", "DESKTOP-3FBTGUL");
            //server.Connect("Kepware.KEPServerEX.V6", "192.168.18.89");

            // 服务启动时间
            DateTime utcTime = server.StartTime;
            TimeZoneInfo chinaTimeZone = TimeZoneInfo.FindSystemTimeZoneById("China Standard Time");

            Console.WriteLine("UTC时间:" + utcTime);
            Console.WriteLine("东八区时间:" + TimeZoneInfo.ConvertTimeFromUtc(server.StartTime, chinaTimeZone));
            // 也可以用.ToLocalTime(),转为本地时间
            Console.WriteLine("东八区时间:" + utcTime.ToLocalTime());

            //服务器的当前时间
            Console.WriteLine(TimeZoneInfo.ConvertTimeFromUtc(server.CurrentTime, chinaTimeZone));

            // 获取服务器状态
            if (server.ServerState == (int)OPCServerState.OPCRunning)
            {
                Console.WriteLine("OPC服务已启动!");
            }

            // 当服务器关闭时 触发
            server.ServerShutDown += (reason) =>
            {
                Console.WriteLine("OPC服务器关闭了!");
            };

            // 地址: 通道 1.设备 1.标记 1
            // 获取所有服务器标记地址
            //OPCBrowser browser = server.CreateBrowser();
            //// 浏览器中拉取分支
            //browser.ShowBranches();
            //// 显示叶子分支
            //browser.ShowLeafs(true);
            //foreach (var tag in browser)
            //{
            //    Console.WriteLine(tag);
            //}

            //string address1 = "通道 1.设备 1.标记 1"; // 自增标记  // 标识对应一个设备点位,需要获取这个点位的数据
            string address1 = "通道 1.设备 1.标记 2"; // 非自增标记(测试写入)
            string address2 = "数据类型示例.16 位设备.K 寄存器.Word1";
            // 需要获取出这个地址对应的值
            // Groups-》Group-》Items-》Item
            // 关于Group: 意思是指需要将请求地址提供给Server进行处理,必须通过以上四个对象进行封装,通过这个结构告诉Server返回什么样的数据
            // 注意:并不是获取OPC服务器中的结构

            OPCGroups groups = server.OPCGroups;
            // 添加一个group,叫做zhaoxi
            OPCGroup group = groups.Add("zhaoxi");

            OPCItems items = group.OPCItems;
            // 第二个参数ClientHandle 是客户端句柄(唯一标识),可以随便设置,但不能重复
            OPCItem item1 = items.AddItem(address1, 10);
            OPCItem item2 = items.AddItem(address2, 11);

            // 直接针对单个标记的读写数据,可以通过OPCItem的Read/Write方法来读写
            item2.Write(200);
            item2.Read((short)OPCAutomation.OPCDataSource.OPCDevice, out object value, out object q, out object t);
            Console.WriteLine("item2读取值:" + value);

            // 执行获取数据动作
            // 1、由Group来获取数据；2、同步读取操作
            // 参数：
            // short Source, 1、表示从缓存中读取数据,2、表示从设备中读取数据
            // int NumItems, 读取标签的数量
            // ref Array ServerHandles, 针对 Item的ServerHandle数组 服务器句柄,是OPC服务器中唯一标识一个标签的句柄,可以通过这个句柄获取数据
            // out Array Values, 读取到的值
            // out Array Errors, 异常信息
            // out object Qualities, 通信质量
            // out object TimeStamps, 时间戳

            // OPC 使用1下标开始,把下标0设置为0
            Array serverHandles = new int[2] { 0, item1.ServerHandle };
            Array values = new object[1] { 0 };
            Array errors = new string[1] { "" };

            // 同步读取数据
            {
                group.SyncRead(
                    (short)OPCAutomation.OPCDataSource.OPCDevice,  // 从设备中读取数据
                    1,  // 读取的最大数量,如果不确定可以设置为0,表示全部读取
                    ref serverHandles,
                    out values,
                    out errors,
                    out object quality, // 质量值为:192 0xC0 Good 良好   枚举:OPCAutomation.OPCQuality.OPCQualityGood
                    out object ts);


                //OPC 读取完后数组下标从1开始
                for (int i = 1; i <= values.Length; i++)
                {
                    Console.WriteLine($"数据结果:{values.GetValue(i)}");
                    Console.WriteLine($"异常信息:{errors.GetValue(i)}");
                }

                Console.WriteLine($"通信质量:{((Array?)quality)?.GetValue(1)}");
                Console.WriteLine($"时间戳:{((DateTime?)((Array?)ts)?.GetValue(1))?.ToLocalTime()}");
            }

            // 异步读取
            {
                // 订阅属性。如果需要进行异步处理必须设置IsSubscribed为true
                // 异步读取实际走的是订阅流,需要设置IsSubscribed=true,否则报错System.Runtime.InteropServices.COMException:“0x80040200”
                group.IsSubscribed = true;
                // 异步回调
                group.AsyncReadComplete += Group_AsyncReadComplete;
                // 参数1,读取标签的数量
                // 参数4,TranscationID(事务标识符),相当于ModbusTcp里的Transaction Identifier事务标识符(请求和响应要相同的tranId),因为发送异步请求不等返回就发下一个请求,那响应的时候怎么去对应你发的请求呢?就是通过这个事务标识符来对应的
                // 参数5,CancelId(取消编号),可以通过这个编号取消异步操作
                group.AsyncRead(1, ref serverHandles, out errors, 100, out int cancelId);

                // 取消异步操作
                // group.AsyncCancel(cancelId);
            }

            // 订阅模式(只在数据变化的时候触发)
            {
                group.IsSubscribed = true;
                // 如果值一直变化，就一直触发DataChange事件(如果值没变化是不会触发的)
                group.DataChange += Group_DataChange;
            }

            #region 写入
            // 同步写入
            serverHandles = new int[2] { 0, item1.ServerHandle };
            values = new object[] { 0, 999 }; // 下标0不使用占位,下标1开始表示标签的值
            errors = new string[1] { "" };
            //group.SyncWrite(1, ref serverHandles, ref values, out errors);

            // 异步写入
            group.IsSubscribed = true; // 异步写入也需要订阅
            group.AsyncWriteComplete += Group_AsyncWriteComplete;
            group.AsyncWrite(1, ref serverHandles, ref values, out errors, 456, out int CancelID);

            #endregion

            Thread.Sleep(1000);
            // 关于ErrorCode 0表示成功,其他值表示错误,1表示函数不正确....
            //string msg = server.GetErrorString(0);
            //Console.WriteLine(msg);

            // 通过GetErrorString获取对应ErrorCode的异常描述
            // 获取所有错误信息，测试10000以上的错误号还有，这里不一一打印，只打印256范围内的
            for (int i = 0; i < 256; i++)
            {
                try
                {
                    string msg = server.GetErrorString(i);
                    Console.WriteLine(i + " - " + msg);
                }
                catch { } // 因为中间有些值没有对应的错误信息,会报错,所以捕获一下
            }

            // 关于Exception异常是com、dcom什么什么的异常，异常的错误号0x8000401a这种的看不出什么错误可以查官网
            // 或OPC Common Questions - COM ErrorCodes.pdf文档(在winform的仓库OPC文件夹里)

            // DCOM-Configuration-Guide.pdf文档是配置DCOM,可以连接远程OPC服务器，需要配置在同一个windows域中(OPC DA),其实可以用OPC UA来替代OPC DA,因为OPC UA(基于TCP)不需要DCOM配置,可以跨平台通信
            Console.ReadKey();
        }

        private static void Group_AsyncWriteComplete(int TransactionID, int NumItems, ref Array ClientHandles, ref Array Errors)
        {
            //if (Errors?.GetValue(1)?.ToString() == "0")
            //{
            //    Console.WriteLine("写入成功!");
            //}

            object? o = Errors.GetValue(1);
            if (o != null)
            {
                Console.WriteLine(server.GetErrorString((int)o));
            }
        }

        private static void Group_AsyncReadComplete(int TransactionID, int NumItems, ref Array ClientHandles,
           ref Array ItemValues, ref Array Qualities, ref Array TimeStamps, ref Array Errors)
        {
            // 关于这个回调
            // 服务端发现订阅的标签对应结果发生变化时,会触发这个回调
            Console.WriteLine(ItemValues.GetValue(1));
        }

        /// <summary>
        /// 订阅方式返回结果回调
        /// </summary>
        private static void Group_DataChange(int TransactionID, int NumItems, ref Array ClientHandles,
            ref Array ItemValues, ref Array Qualities, ref Array TimeStamps)
        {
            Console.WriteLine(ItemValues.GetValue(1));
        }




    }
}
